/*  

Cafechess Library: Cover basic chess programming needs, like GUI, Engine integration, 
FEN and PGN parsing.

                 \\|//
                 (o o)
--------------ooO-(_)-Ooo----------------------------------
Copyright (C) 2009  By Gregory A. Prentice Snoqualmie, WA
                    www.cafechess.org
                    cafechess@comcast.net
                      Oooo
--------------oooO----(  )---------------------------------
              (  )    (_/
               \_)

Microsoft Public License (Ms-PL)
               
Revisions:

Author        Date    Description:

Greg Prentice 4-21-05 Fixed similar lines where crafty has re-evaluated due to deeper
                      analisys of the position.

*/
using System;
using System.Windows.Forms;
using System.ComponentModel;
using System.Data;
using System.Text.RegularExpressions;

namespace Cafechess.Chess.Engines.WBD
{ 
  /// <summary>
  /// The default filtering class for crafty.  
  /// </summary>
  public class CraftyFilter: IFilterAnalysis
  {
    #region IFilterAnalysis Members
    /// <summary>
    /// First determines if the line currently exists or if it is the begining
    /// of a deeper analyzed variation.  In either case we want to update the existing
    /// line.  If the line does not exist than we assume a new line must be added.
    /// Currently this routine is the default filter for the Crafty output from the 
    /// analysis mode.  It attempts to filter out any duplicate lines and will signal an
    /// updated is needed if the current line already exists or if the new line is the
    /// subvariation of a longer line.  It will signal an insert of a new record if it
    /// does not already exist and discard any similar lines with a worse score.
    /// </summary>
    /// <param name="sideToMove"></param>
    /// <param name="table"></param>
    /// <param name="score"></param>
    /// <param name="line"></param>
    /// <param name="hash"></param>
    /// <returns>-1 = replace existing row, 0 = ignore this row, 1 = add new row</returns>
    public int Filter(bool sideToMove, DataTable table, double score, string line, ref int hash)
    {
      DataRow foundRow=null;
      int results = CraftyAnalysis.AddNew;

      // See if the line already exists.  This is the Quick Way.
      DataRow[] foundRows = table.Select(string.Format("hash = {0}",line.GetHashCode()));
      if(foundRows != null && foundRows.Length > 0 )
      {
        // Ok get the row and test which one we want to keep.
        foundRow = foundRows[0];
        double oldScore = Convert.ToDouble(foundRow[CraftyAnalysis.Score]);
        // Color specific test.
        if(sideToMove)
        {
          // Whites score is better the more positive it is.
          if( score > oldScore )
          {
            // Replace existing.
            results = CraftyAnalysis.AddExisting;
            hash = (int)foundRow[CraftyAnalysis.Hash];
          }
          else
          {
            results = CraftyAnalysis.AddNone;
          }
        }
        else
        {
          // Blacks score is better the more negative it is.
          if( score < oldScore )
          {
            // Replace existing.
            results = CraftyAnalysis.AddExisting;
            hash = (int)foundRow[CraftyAnalysis.Hash];
          }
          else
          {
            results = CraftyAnalysis.AddNone;
          }
        }
      }
      else
      {
        // We have to look for new lines that have been re-evaluated
        // by Crafty and now have a longer tree and a different score.
        // We should then update the current row with the new line and score.
        foreach(DataRow checkRow in table.Rows)
        {
          string oldLine = checkRow[CraftyAnalysis.Line] as string;
          if( line.StartsWith(oldLine) )
          {
            // Replace existing row.
            results = CraftyAnalysis.AddExisting;
            hash = (int)checkRow[CraftyAnalysis.Hash];
            break;
          }
          else
            results = CraftyAnalysis.AddNew;
        }
      }
      return results;
    }
    public int GetHashCode(string str)
    {
      return str.GetHashCode();
    }

    #endregion

  }

  /// <summary>
  /// Public class that allows the parsing of analysis data from the crafty
  /// chess engines and displaying it from another thread into the main thread
  /// of a UI text box.  This is important as crafty runs in it's own thread as
  /// well as the routines the pull crafty's output into our program.  You can
  /// only update a UI from the main thread.
  /// </summary>
  public class CraftyAnalysis
  {
    Regex coParser;
    DataTable coTable;
    DataView coView;
    DataSet coDataSet;
    System.Windows.Forms.TextBox coControl;
    IFilterAnalysis coFilter;
    bool coColor;
    int coMaximum;
    bool coSort;

    public const int Ply = 0;
    public const int Score = 1;
    public const int Time = 2;
    public const int Nodes = 3;
    public const int Line = 4;
    public const int Hash = 5;


    public const int AddExisting = -1;
    public const int AddNone = 0;
    public const int AddNew = 1;
    
    /// <summary>
    /// Used to set a new filter class for the analysis returned by crafty.
    /// Normally used if the end user wishes to change the default behavior of
    /// how the analysis output is formatted.
    /// </summary>
    public IFilterAnalysis Filter
    {
      set{ coFilter = value; }
      get{ return coFilter; }
    }

    public bool IsSorting
    {
      get{ return coSort; }
      set{ coSort = value; }
    }

    /// <summary>
    /// When true we are displaying for white so higher
    /// scores get displayed first, opposite for black.
    /// </summary>
    public bool SideToMove
    {
      get{return coColor;}
      set{coColor = value;}
    }    
    // This is our public access to the data.
    public DataTable AnalysisData
    {
      get{ return coTable; }
    }
    /// <summary>
    /// Maximum rows to display.
    /// </summary>
    public int Maximum
    {
      get{return coMaximum;}
      set{coMaximum = value;}
    }
    /// <summary>
    /// Public constructor that allows the passing of a gui chessbox
    /// to display data in.
    /// </summary>
    /// <param name="control"></param>
    public CraftyAnalysis(System.Windows.Forms.TextBox  control)
    {
      // Display all.
      coMaximum=0;
      coControl = control;
      coUIEventHandler = new UIEventHandler(UpdateUI);
      SideToMove = false;
      coDataSet = new DataSet("AdditionalInfo");   
      coTable = new DataTable("Analysis");
      DataColumn ply = new DataColumn("ply", typeof(string));
      DataColumn score = new DataColumn("score", typeof(double));
      DataColumn time = new DataColumn("time", typeof(string));
      DataColumn nodes = new DataColumn("nodes", typeof(string));
      DataColumn line = new DataColumn("line", typeof(string));
      DataColumn hash = new DataColumn("hash", typeof(int));

      coTable.Columns.Add(ply);
      coTable.Columns.Add(score);
      coTable.Columns.Add(time);
      coTable.Columns.Add(nodes);
      coTable.Columns.Add(line);
      coTable.Columns.Add(hash);
      
      DataColumn[] keys = new DataColumn[1];
      keys[0] = hash;      
      
      coTable.PrimaryKey = keys;
      // Add the tables to the DataSet.
      coDataSet.Tables.Add(coTable);
      coView = new DataView(coTable);
      // Init our analysis data parser.
      coParser =  new Regex( @"([0-9]+)[^-\w]+(-?[0-9]+)\W+([0-9]+)\W+([0-9]+)\W+(.*)",
        RegexOptions.Compiled);      
      Filter = new CraftyFilter();
    }
    /// <summary>
    /// Determines if this is a new line or an existing line then adds it to
    /// our analysis data.
    /// </summary>
    /// <param name="analysis"></param>
    /// <returns></returns>
    public bool Add(string analysis)
    {
      bool ok=false;

      Match aMatch;
      aMatch = coParser.Match(analysis);     
      if( aMatch.Success && aMatch.Groups.Count == 6 )
      {
        // Pre-calc our score and time.
        string ply = aMatch.Groups[1].ToString();
        double score = double.Parse(aMatch.Groups[2].ToString()) * .01;
        double time = double.Parse(aMatch.Groups[3].ToString()) * .01;
        string nodes = aMatch.Groups[4].ToString();
        // Get the pgn line and compute the hash value to see if our record
        //  already exists in the table.
        string line = aMatch.Groups[5].ToString();
        int hash = line.GetHashCode();
        DataRow row;
        if(Filter != null )
        {
          switch(Filter.Filter(SideToMove,coTable,score,line,ref hash))
          {
            case CraftyAnalysis.AddExisting:
              // The hash value of the work row will be set to the one we need to find.
              DataRow[] foundRows = coTable.Select(string.Format("hash = {0}",hash));
              if(foundRows != null && foundRows.Length > 0 )
              {
                // Found the row so simply update the row with the new data.
                lock(coTable)
                {
                  row = foundRows[0];
                  row[CraftyAnalysis.Ply] = ply;
                  row[CraftyAnalysis.Score] = score;
                  row[CraftyAnalysis.Time] = time;
                  row[CraftyAnalysis.Nodes] = nodes;
                  row[CraftyAnalysis.Line] = line;                  
                }
              }
              break;
            case CraftyAnalysis.AddNone:
              break;
            case CraftyAnalysis.AddNew:
              // We need to add a brand new line so add it here.
              lock(coTable)
              {
                row = coTable.NewRow();
                row[CraftyAnalysis.Ply] = ply;
                row[CraftyAnalysis.Score] = score;
                row[CraftyAnalysis.Time] = time;
                row[CraftyAnalysis.Nodes] = nodes;
                row[CraftyAnalysis.Line] = line;
                row[CraftyAnalysis.Hash] = Filter.GetHashCode(line);
                coTable.Rows.Add(row);
              }           
              break;
          }
        }
        ok=true;        
      }
      return ok;
    }
    /// <summary>
    /// Remove all of our rows from the public table.
    /// </summary>
    public void Clear()
    {
      coTable.Rows.Clear();
    }

    /// <summary>
    /// Call when you want the UI text box to be updated with the values.
    /// </summary>
    public void DisplayAnalysis()
    {
      if(coControl.InvokeRequired)
        coControl.Invoke(coUIEventHandler);
      else
        UpdateUI();
    }

    /// <summary>
    /// Definition of delegate used for updating the UI.
    /// </summary>
    public delegate void UIEventHandler();
    /// <summary>
    /// Declare our Delegate
    /// </summary>
    UIEventHandler coUIEventHandler;
    /// <summary>
    /// This is the UIEventHandler delegate that get's called to actually
    /// update the user interface.
    /// </summary>
    void UpdateUI()
    {
      lock(this)
      {
        // Determine our sorting
        if( coSort )
        {
          if( SideToMove )
            coView.Sort = "score DESC";
          else
            coView.Sort = "score ASC";
        }
        else
        {
          coView.Sort = null;
        }
        // Clear out any text being displayed.
        coControl.Clear();
        // Show all lines
        for( int ndx = 0; ndx < coView.Count; ndx++)
        {
          // Only show the maximum number of rows that the user wants to see
          //  zero means show them all.
          if( coMaximum > 0 && ndx >= coMaximum )
            break;
          DataRowView aRow = coView[ndx];
          coControl.AppendText(string.Format("{0:F}\t{1}\r\n",aRow[1],aRow[4] ));
            
        }
      } 
    }
  }
}

